function PTZScene(id, origin, pan_range, tilt_range, zoom_range, width, height, total_pan_degrees, total_tilt_degrees){
	this.id = id;
	this.origin = origin;
	this.pan_range = pan_range;
	this.tilt_range = tilt_range;
	this.zoom_range = zoom_range;
	this.width = width;
	this.height = height;
	this.total_pan_degrees = 320;
	this.total_tilt_degrees = 120;
	this.total_pan_rad = this.total_pan_degrees * Math.PI / 180;
	this.total_tilt_rad = this.total_tilt_degrees * Math.PI / 180;
	
	this.x = new Array();
	this.y = new Array();
	this.z = new Array();
	this.u = new Array();
	this.v = new Array();
	this.pan_normals = new Array();
	this.tilt_normals = new Array();
	this.zooms = new Array();
	this.indices = new Array();
}

// adds a correspondence to the scene, and automatically converts lat/lon/alt into
// XYZ, as well as normalizing the pan/tilt/zoom values.
PTZScene.prototype.add_correspondence = function(id, lat,lon,alt,u,v,pan,tilt,zoom){
	var i = this.x.length;
	this.pan_normals[i]  = (pan - this.pan_range[0]) / (this.pan_range[1] - this.pan_range[0]);
	this.tilt_normals[i] = (tilt - this.tilt_range[0]) / (this.tilt_range[1] - this.tilt_range[0]);
	this.zooms[i] = zoom;
	
	var meters_per_lat = distance(this.origin.lat, this.origin.lon, 0, this.origin.lat+0.00001, this.origin.lon, 0) / 0.00001;
	var meters_per_lon = distance(this.origin.lat, this.origin.lon, 0, this.origin.lat, this.origin.lon+0.00001, 0) / 0.00001;
	this.x[i] = (lon - this.origin.lon) * meters_per_lon;
	this.y[i] = (lat - this.origin.lat) * meters_per_lat;
	this.z[i] = (alt - this.origin.alt);
	
	this.u[i] = u;
	this.v[i] = v;
	this.indices[i] = id;
}

// some functions to convert a solution vector into an object and back.
function solution_vector2struct(sol_vector){
    return {tx: sol_vector[0],
            ty: sol_vector[1],
            tz: sol_vector[2],
            pan_offset: sol_vector[3],
            tilt_offset: sol_vector[4],
            roll_offset: sol_vector[5],
            focal_f1: sol_vector[6],
            focal_f2: sol_vector[7],
            focal_f3: sol_vector[8],
            total_pan_degrees: sol_vector[9],
            total_tilt_degrees: sol_vector[10]};
}

function solution_struct2vector(sol_struct){
    return [sol_struct.tx, 
           sol_struct.ty, 
           sol_struct.tz, 
           sol_struct.pan_offset, 
           sol_struct.tilt_offset, 
           sol_struct.roll_offset,
           sol_struct.focal_f1, 
           sol_struct.focal_f2, 
           sol_struct.focal_f3,
           sol_struct.total_pan_degrees,
           sol_struct.total_tilt_degrees]
}

// the initial guess for a PTZ calibration solution.
PTZScene.prototype.initial_solution = function(){
	var initial = {tx: mean(this.x),
	               ty: mean(this.y),
	               tz: mean(this.z),
	               pan_offset: 0,
	               tilt_offset: 0,
	               roll_offset: 0,
	               focal_f1: 12.5 * this.width, //100 * mean(this.zooms),
	               focal_f2: 2.5 * this.width,
	               focal_f3: 1.25 * this.width,
	               total_pan_degrees: 340,
	               total_tilt_degrees: 120}
    return solution_struct2vector(initial);
}



PTZScene.prototype.initial_step = function(){
	var stepSizes = {tx: std(this.x)/2,
	                 ty: std(this.y)/2,
	                 tz: std(this.z)/2,
	                 pan_offset: Math.PI / 4, //4
	                 tilt_offset: Math.PI / 90, // 20
	                 roll_offset: 0,
	                 focal_f1: 100,
	                 focal_f2: 50,
	                 focal_f3: 20,
	                 total_pan_degrees: 20,
	                 total_tilt_degrees: 10};
	 return solution_struct2vector(stepSizes);
}


PTZScene.prototype.constraints_satisfied = function(sol_struct){
    if (sol_struct.focal_f1 < 0 || sol_struct.focal_f2 < 0 || sol_struct.focal_f3 < 0){
        return false;
    }
    
    if (sol_struct.total_pan_degrees > 360 || sol_struct.total_tilt_degrees > 180 || sol_struct.total_pan_degrees < 0 || sol_struct.total_tilt_degrees < 0){
        return false;
    }
    
    if (sol_struct.focal_f2 > 0.5 * sol_struct.focal_f1 || sol_struct.focal_f3 > sol_struct.focal_f2){
        return false;
    }
    
    if (sol_struct.pan_offset > Math.PI * 2){
        return false;
    }
    
    if (sol_struct.tilt_offset > Math.PI * 2){
        return false;
    }
    
    return true;
}


PTZScene.prototype.calibrate = function(iterations, epsilon, monitor, submit){	
    var me = this;
    
    var error_function = function(sol){
        return me.total_angular_error(sol);
    }
    
    var n = this.x.length;
	var s = new Simplex(error_function, this.initial_solution(), this.initial_step());
	var solution = s.minimize(epsilon * n, iterations, false);
	
	// solve a second optimization to really nail down the pan and tilt offsets.
	//var s = new Simplex(error_function, solution, this.fine_step())
	//var solution = s.minimize(epsilon * n, iterations, false);
	
	
	if (monitor){
		this.display_solution(solution);
	}
	if (submit){
	    this.submit_solution(solution, monitor);
    }
	return {error: this.total_angular_error(solution), solution: solution};
}


// submits a solution to the 
PTZScene.prototype.submit_solution = function(solution, monitor){
    var s = solution_vector2struct(solution);
    
    var lats_per_meter = latitude_per_meter(this.origin.lat,this.origin.lon)
    var lons_per_meter = longitude_per_meter(this.origin.lat,this.origin.lon)
    
	var camera_lon = s.tx * lons_per_meter + this.origin.lon;
	var camera_lat = s.ty * lats_per_meter + this.origin.lat;
	var cameraAlt = s.tz + this.origin.alt;
	
	var pan_offset         = s.pan_offset;
	var tilt_offset        = s.tilt_offset;
	var roll_offset        = s.roll_offset;
	
	var focal_f1          = s.focal_f1;
	var focal_f2          = s.focal_f2;
	var focal_f3          = s.focal_f3;
	
	var total_pan_degrees = s.total_pan_degrees;
	var total_tilt_degrees = s.total_tilt_degrees;
	
	var params = '&scene=' + this.id;
	params = params + '&originLat=' + this.origin.lat + '&originLon=' + this.origin.lon + '&originAlt=' + this.origin.alt;
	params = params + '&tx=' + s.tx + '&ty=' + s.ty + '&tz=' + s.tz;
	params = params + '&cameraLat=' + camera_lat + '&cameraLon=' + camera_lon + '&cameraAlt=' + cameraAlt;
	params = params + '&panOffsetRad=' + pan_offset + '&tiltOffsetRad=' + tilt_offset + '&rollOffsetRad=' + roll_offset;
	params = params + '&focalLengthF1=' + focal_f1 + '&focalLengthF2=' + focal_f2 + '&focalLengthF3=' + focal_f3;
	params = params + '&totalPanDegrees=' + total_pan_degrees + '&totalTiltDegrees=' + total_tilt_degrees;

	var http = createRequest();
	var url = 'setCalibration';
	var me = this;
	http.open("POST", url, true);
	
	http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	http.setRequestHeader("Content-length", params.length);
	http.setRequestHeader("Connection", "close");	
	
	http.onreadystatechange = function(){
		if(http.readyState == 4){
			if (!monitor){
				window.location = "viewScene?scene=" + me.id;
			}
		}
	};
	http.send(params);
}

PTZScene.prototype.display_solution = function(solution){
    var s = solution_vector2struct(solution);
    
	var lats_per_meter = latitude_per_meter(this.origin.lat,this.origin.lon)
    var lons_per_meter = longitude_per_meter(this.origin.lat,this.origin.lon)
    
	var camera_lon = s.tx * lons_per_meter + this.origin.lon;
	var camera_lat = s.ty * lats_per_meter + this.origin.lat;
	var cameraAlt = s.tz + this.origin.alt;
	
	var pan_offset         = s.pan_offset;
	var tilt_offset        = s.tilt_offset;
	var roll_offset        = s.roll_offset;
	
	var focal_f1          = s.focal_f1;
	var focal_f2          = s.focal_f2;
	var focal_f3          = s.focal_f3;
	
	var total_pan_degrees = s.total_pan_degrees;
	var total_tilt_degrees = s.total_tilt_degrees;
	var total_pan_rad = total_pan_degrees * Math.PI / 180;
	var total_tilt_rad = total_tilt_degrees * Math.PI / 180;
	
	var resultDiv = document.getElementById('result');
	resultDiv.innerHTML =  'Location: (' + camera_lat + ',' + camera_lon + ',' + cameraAlt + ')<br/>'
	resultDiv.innerHTML = resultDiv.innerHTML + 'Rotation: (' + pan_offset + ',' + tilt_offset+ ',' + roll_offset + ')<br/>'
	resultDiv.innerHTML = resultDiv.innerHTML + 'Focal: (' + focal_f1 + ',' + focal_f2 + ',' + focal_f3 + ')<br/>'
	resultDiv.innerHTML = resultDiv.innerHTML + 'total pan/tilt: (' + total_pan_rad + ',' + total_tilt_rad + ')<br/>'
	resultDiv.innerHTML = resultDiv.innerHTML + this.x.length + ' correspondences total.<br/>'

	
	this.dump_individual_errors(solution);
}

PTZScene.prototype.dump_individual_errors = function(solution){
    var allM = this.make_many_projection_matrices(solution);
    
	var n = this.x.length;
	var sErr = 0;
	var sErrRad = 0;
	var sErrDeg = 0;
	
	var errTable = '<table><tr><th>ID</th><th>Error [0,2] </th><th>Error in Radians</th><th>Error in Degrees</th></tr>'
	for (var i = 0; i < n; i++){
		var M = allM[i];
		errTable = errTable + '<tr><td>' + this.indices[i] + '</td>' 
		var error = this.angular_error(M, solution, this.u[i], this.v[i], this.x[i], this.y[i], this.z[i])
		errTable = errTable + '<td>' + error + '</td>'
		var errRad = Math.acos(1-error);
		var errDeg = 180.0 / Math.PI * errRad;
		
		errTable = errTable + '<td>' + errRad + '</td>'
		errTable = errTable + '<td>' + errDeg + '</td></tr>'
		
		sErr = sErr + error;
		sErrRad = sErrRad + errRad;
		sErrDeg = sErrDeg + errDeg;
	}
	errTable = errTable + '<tr><td>Sum</td><td>' + sErr + '</td><td>' + sErrRad + '</td><td>' + sErrDeg + '</td></tr>'
	errTable = errTable + '<tr><td>Average</td><td>' + (sErr/n) + '</td><td>' + (sErrRad/n) + '</td><td>' + (sErrDeg/n) + '</td></tr>'
	errTable = errTable + '</table>'
}

PTZScene.prototype.make_projection_matrix = function(panNorm, tiltNorm, zoom, solution){
    var s = solution_vector2struct(solution);
    var width = this.width;
    var height = this.height;
    var tx = s.tx;
    var ty = s.ty;
    var tz = s.tz;
    var pan_offset = s.pan_offset;
    var tilt_offset = s.tilt_offset;
    var roll_offset = s.roll_offset;
    var total_pan_rad = s.total_pan_degrees * Math.PI / 180;
    var total_tilt_rad = s.total_tilt_degrees * Math.PI / 180;

    var a = s.focal_f1;
    var b = s.focal_f2;
    var c = s.focal_f3;
    var zmax = this.zoom_range[0]
    var zmin = this.zoom_range[1]
    var zh = (zmax + zmin) / 2;

    var sub1 = -a * b * zh + b * c * zh + a * c * zmax - b * c * zmax + a * b * zmin -a * c * zmin;
    var sub2 = -a * zh + c * zh + a * zmax - b * zmax + b * zmin -c * zmin;
    var sub3 = b * zh * zmax - c * zh * zmax + a * zh * zmin - b * zh * zmin - a * zmax * zmin +c * zmax * zmin;

    var beta = (sub3)/(-sub2)
    var gamma = (sub1)/(sub2)
    var alpha = b * zh - zh * gamma - b * beta + beta * gamma;
    
    var focal = alpha / (zoom - beta) + gamma;
	
	var K = new Matrix(3,3,[[focal, 0,      width / 2],
						    [0,                   -focal, height / 2],
						    [0,                   0,      1]        ]);

	// R is the rotation matrix.
	var alpha = -(panNorm * total_pan_rad + pan_offset);
	var beta = tiltNorm * total_tilt_rad + tilt_offset;
	var gamma = roll_offset + Math.PI;
	
	var ca = Math.cos(alpha)
	var cb = Math.cos(beta)
	var cg = Math.cos(gamma)
	var sa = Math.sin(alpha)
	var sb = Math.sin(beta)
	var sg = Math.sin(gamma)
	
	var R = new Matrix(3,3, [[ca * cg - cb * sa * sg,  cg * sa + ca * cb * sg, sb * sg],
							[-cb * cg * sa - ca * sg, ca * cb * cg - sa*sg,   cg * sb],
							[sa * sb,                 -ca * sb,               cb]])
	// T is the translation vector.
	var center = new Matrix(1,3,[[tx],[ty],[tz]])
	var T = R.multiply(center);
	
	R = R.values;
	T = T.values;
	
	// M is the full projection matrix.
	var tmp = new Matrix(4,3, [[R[0][0], R[0][1], R[0][2], -T[0][0]],
							 [R[1][0], R[1][1], R[1][2], -T[1][0]],
							 [R[2][0], R[2][1], R[2][2], -T[2][0]]]);
	var M = K.multiply(tmp);
	
	return M;
}

PTZScene.prototype.make_many_projection_matrices = function(solution){
	// Input: intrinsic and extrinsic parameters of a camera across many pan,tilt and zoom configurations.
	// Output: the 3x4 projection matrices that satisfy these parameters.
    var s = solution_vector2struct(solution);
	var allMatrices = new Array();
	var n = this.pan_normals.length;
	for (var i = 0; i < n; i++){
		var panNorm = this.pan_normals[i];
		var tiltNorm = this.tilt_normals[i];
		var zoom = this.zooms[i];
    	
		var mat = this.make_projection_matrix(panNorm, tiltNorm, zoom, solution)
		
		allMatrices[i] = mat;
	}
	return allMatrices;
}

PTZScene.prototype.total_angular_error = function(sol){
//	Returns the sum of all angular errors.
//	sol                                : a 10-element solution vector, where
//	sol[0:2]=tx,ty,tz               :    the position of the camera in ENU space.
//	sol[3:5]=pan0,tilt0,roll0       :    the rotational offsets of the camera.
//	sol[6:8]=focal_f1, focal_f2, focal_f3     :  Taylor series approximation for focal function f(z) = f1 + f2 * z + (f3 * z^2)/2
//	sol[8:9]=dpan, dtilt            :    the total field of view of the camera in pan and tilt.
//Output:  the angular difference error.
	
	
	var n = this.x.length;
	var s = solution_vector2struct(sol);
	
	/*
	var tx                = s.tx;
	var ty                = s.ty;
	var tz                = s.tz
	var pan_offset         = s.pan_offset;
	var tilt_offset        = s.tilt_offset
	var roll_offset        = 0;
	var focal_f1          = s.focal_f1;
	var focal_f2          = s.focal_f2;
	var focal_f3          = s.focal_f3;
	var total_pan_degrees = s.total_pan_degrees;
	var total_tilt_degrees = s.total_tilt_degrees
	var total_pan_rad = total_pan_degrees / 180.0 * Math.PI;
	var total_tilt_rad = total_tilt_degrees / 180.0 * Math.PI;
	*/
	
	if (!this.constraints_satisfied(s)){
		return Infinity;
	}

	var allM = this.make_many_projection_matrices(sol);
	
	var error = 0;
	
	// weight the correspondences by how zoomed in they are.
	// (i.e., the more zoomed in, the more the angular error matters).
	var max_zoom = max(this.zooms);
	var min_zoom = min(this.zooms);
	var sigma =  0.75 * (max_zoom - min_zoom);
	var sigma_2 = sigma * sigma;
	var weights = new Array();
	for (var i = 0; i < n; i++){
	    // weight each example in [0.1, 1.1]
	    weights[i] = (this.zooms[i] - max_zoom) / (min_zoom - max_zoom) + 0.1;
	    
	    // weight is a Gaussian centered around minimum zoom level
	    //var d = (this.zooms[i] - min_zoom);
	    //weights[i] = Math.exp(-d*d / sigma_2)
	}
	
	// normalize.
	var sum_weight = sum(weights);
	for (var i = 0; i < n ; i++){
	    weights[i] = weights[i] / sum_weight;
	}
	
	
	
	for (var i = 0; i < n; i++){
		var M = allM[i];
		
		error = error + weights[i] * this.angular_error(M, sol, this.u[i], this.v[i], this.x[i], this.y[i], this.z[i]);
	}
	
	return error;
}

PTZScene.prototype.angular_error = function(M, sol, u, v, x, y, z){
    
	// from M and the image coordinates, find out which ray goes from the
	// camera center to the image through the user-provided image coordinates.
	
	var s = solution_vector2struct(sol);
	
	// solving for XYZ:
	//   x*d = m11*X + m12*Y + m13*Z + m14
	//   y*d = m21*X + m22*Y + m23*Z + m24
	//   d = m31*X + m32*Y + m33*Z + m34
	//   (x*d - m14) = m11*X + m12*Y + m13*Z
	//   (y*d - m24) = m21*X + m22*Y + m23*Z 
	//   d - m34  = m31*X + m32*Y + m33*Z
	M = M.values;
	var w = -100;
	var mat = new Matrix(4,3, [[M[0][0], M[0][1], M[0][2], (u * w - M[0][3])], 
							   [M[1][0], M[1][1], M[1][2], (v * w - M[1][3])],
							   [M[2][0], M[2][1], M[2][2], ( w - M[2][3])]]
						 );
	var vals = mat.rowEchelon().values;
	var projected_ray = new Array(vals[0][3] - s.tx, vals[1][3] - s.ty, vals[2][3] - s.tz);
	
	var mag = Math.sqrt(projected_ray[0]*projected_ray[0] + 
						projected_ray[1]*projected_ray[1] + 
						projected_ray[2]*projected_ray[2])
	for (var j = 0; j < 3; j++){
		projected_ray[j] = projected_ray[j] / mag;
	}
	
	// now find the ray that goes from the camera center through the user-provided 3D XYZ coordinate.
	var ray_xyz_point = new Array(x - s.tx, y - s.ty, z - s.tz);
	var mag = Math.sqrt(ray_xyz_point[0]*ray_xyz_point[0] + 
						ray_xyz_point[1]*ray_xyz_point[1] +
						ray_xyz_point[2]*ray_xyz_point[2])
	for (var j = 0; j < 3; j++){
		ray_xyz_point[j] = ray_xyz_point[j] / mag;
	}
	//the error increases as the vectors become more and more opposite.
	// i.e., error = 1 - ray1 (dot) ray2.
	return 1 - (projected_ray[0] * ray_xyz_point[0] + 
			projected_ray[1] * ray_xyz_point[1] + 
			projected_ray[2] * ray_xyz_point[2]);
}